#!/usr/bin/python

"""
CachedFormWizard - is an alternate implementation of Django Contrib FormWizard.

It implements a database cache for a given set of forms and stored form data in
the database for each form/step in a pre-validation serialized format. This
allows for navigation back and forth between forms and also saving incomplete
forms for completion at a later date.

For usage details, please see the documentation and the sample app.
"""

__author__ = 'Sumeet Singh (sumeets@singhsays.com)'


# Imports
from django.template.context import RequestContext
from django.shortcuts import render_to_response
from models import FormsCache


class CachedFormWizard(object):
  """Class representing a Cached FormWizard."""

  # Dictionary of extra template context variables.
  extra_context = {}

  # The HTML (and POST data) field name for the "step" variable.
  step_field_name = "wizard_step"

  def __init__(self,
               instance,
               form_list,
               initial=None,
               template_name='forms/wizard.html'):
    """Constructor for CachedFormWizard

    Args:
      instance (string) unique string representing the associated cache.
      form_list (list) list of form classes (not instances).
      initial (dict) dictionary of initial values for forms.
    """
    self.form_list = form_list[:]
    self.initial = initial or {}
    self.template = template_name
    # A zero-based counter keeping track of which step we're in.
    self.step = 0
    # An instance of FormsCache to hold the cache data.
    self.cache = FormsCache.get_cache(instance=str(instance))

  def __repr__(self):
    return "step: %d\nform_list: %s\ninitial_data: %s" % (
        self.step, self.form_list, self.initial)

  def get_form(self, step, data=None):
    """Helper method to instantiate and return the Form for a given step."""
    return self.form_list[step](
        data,
        prefix=str(step),
        initial=self.initial.get(step, None))

  def num_steps(self):
    """Helper method that returns the total number of steps."""
    return len(self.form_list)

  def __call__(self, request, *args, **kwargs):
    """Main request handler method."""
    if 'extra_context' in kwargs:
      self.extra_context.update(kwargs['extra_context'])
    current_step = self.determine_step(request, *args, **kwargs)
    self.parse_params(request, *args, **kwargs)

    # Sanity check.
    if current_step >= self.num_steps():
      return self.final_validation(request, *args, **kwargs)

    # Process the current step. If it's valid, go to the next step or call
    # done(), depending on whether any steps remain.
    if request.method == 'POST':
      form = self.get_form(current_step, request.POST)
      if form.is_valid():
        self.cache.set_form(current_step, request.POST)
        self.process_step(request, form, current_step)
        next_step = current_step + 1

        # If this was the last step, validate all of the forms one more
        # time, as a sanity check, and call done().
        num = self.num_steps()
        if next_step == num:
          return self.final_validation(request, *args, **kwargs)
          #final_form_list = [self.get_form(i, self.cache.get_form(i))
          #    for i in range(num)]

          ## Validate all the forms. If any of them fail validation, that
          ## must mean the validator relied on some other input, such as
          ## an external Web site.
          #for i, f in enumerate(final_form_list):
          #  if not f.is_valid():
          #    return self.render_revalidation_failure(request, i, f)
          #return self.done(request, final_form_list)

        # Otherwise, move along to the next step.
        else:
          form = self.get_form(next_step, self.cache.get_form(next_step))
          self.step = current_step = next_step
    else:
      data = self.cache.get_form(current_step)
      if data:
        form = self.get_form(current_step, data)
      else:
        form = self.get_form(current_step)
    return self.render(form, request, current_step)

  def final_validation(self, request, *args, **kwargs):
    """Method to run final validation pass after all forms are filled."""
    num = self.num_steps()
    final_form_list = [self.get_form(i, self.cache.get_form(i))
        for i in range(num)]
    for i, f in enumerate(final_form_list):
      if not f.is_valid():
        return self.render_revalidation_failure(request, i, f)
    return self.done(request, final_form_list)

  def render(self, form, request, step, context=None):
    """Renders the given Form object, returning an HttpResponse."""
    return self.render_template(request, form, step, context)

  def render_revalidation_failure(self, request, step, form):
    """Hook for rendering a template if final revalidation failed."""
    return self.render(form, request, step)

  def determine_step(self, request, *args, **kwargs):
    """Returns the current step."""
    data = self.cache.get_data()
    if data:
      last_step = max(data.keys(), key=int)

      # if we want to jump to a specific step.
      # ensure we are jumping to valid step.
      # i.e a step which is within the step that we have already filled.
      if 'step' in kwargs and kwargs['step']:
        step = int(kwargs['step'])
        if step <= int(last_step):
          return step

      # if we want to skip to the last filled form.
      if 'skip_to_last' in kwargs and kwargs['skip_to_last']:
        return int(last_step) + 1
      else:
        return 0

    if not request.POST:
      return 0

    try:
      step = int(request.POST.get(self.step_field_name, 0))
    except ValueError:
      return 0

    return step

  def parse_params(self, request, *args, **kwargs):
    """Hook for setting some state."""
    pass

  def render_template(self, request, form, step, context=None):
    """Renders the template for the given step. Override this method if you
    want to add a custom context, return a different MIME type, etc. If you
    only need to override the template name, use get_template() instead.

    Returns:
      returns an HttpResponse object.
    """
    context = context or {}
    context.update(self.extra_context)
    return render_to_response(
        self.template,
        dict(
            context,
            step_field=self.step_field_name,
            step0=step,
            step=step + 1,
            step_count=self.num_steps(),
            form=form),
        context_instance=RequestContext(request))

  def process_step(self, request, form, step):
    """Hook for modifying the FormWizard's internal state, given a fully
    validated Form object. The Form is guaranteed to have clean, valid
    data.

    This method should *not* modify any of that data. Rather, it might want
    to set self.extra_context or dynamically alter self.form_list, based on
    previously submitted forms.

    Note that this method is called every time a page is rendered for *all*
    submitted steps.
    """
    pass

  def done(self, request, form_list):
    """Hook for doing something with the validated data.
    This method is responsible for the final processing.
    """
    raise NotImplementedError(
        "Your %s class has not defined a done() method, which is required."
        % self.__class__.__name__)
